package cn.kinoko.service.auth.impl;

import cn.hutool.core.util.RandomUtil;
import cn.kinoko.common.code.ErrorCodeEnum;
import cn.kinoko.common.constant.CacheKeyConstant;
import cn.kinoko.common.utils.BCrypt;
import cn.kinoko.common.utils.CaptchaUtil;
import cn.kinoko.common.utils.ExceptionUtil;
import cn.kinoko.model.auth.req.EmailCodeReq;
import cn.kinoko.model.auth.req.LoginReq;
import cn.kinoko.model.auth.req.RegisterReq;
import cn.kinoko.model.auth.req.ResetPwdReq;
import cn.kinoko.model.auth.vo.CaptchaVO;
import cn.kinoko.model.auth.vo.TokenAuthVO;
import cn.kinoko.model.email.Mail;
import cn.kinoko.model.user.constant.UserSexEnum;
import cn.kinoko.model.user.constant.UserStateEnum;
import cn.kinoko.model.user.entity.UserInfo;
import cn.kinoko.model.user.req.UserInfoSaveReq;
import cn.kinoko.service.auth.AuthService;
import cn.kinoko.service.auth.tokenGranter.GranterParams;
import cn.kinoko.service.auth.tokenGranter.TokenGranter;
import cn.kinoko.service.auth.tokenGranter.TokenGranterBuilder;
import cn.kinoko.service.common.CommonService;
import cn.kinoko.service.email.EmailService;
import cn.kinoko.service.user.UserInfoService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.Maps;
import com.wf.captcha.base.Captcha;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Author: kk
 * @Date: 2023/2/12  17:13
 * @Version 1.0
 */
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {

    // 验证码缓存，1分钟过期
    private final Cache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100_000)
            .expireAfterAccess(1, TimeUnit.MINUTES)
            .build();
    @Resource
    private UserInfoService userInfoService;
    @Resource
    private CommonService commonService;
    @Resource
    private EmailService emailService;

    /**
     * 验证码生成
     *
     * @param request 请求信息
     * @return 验证码视图
     */
    @Override
    public CaptchaVO buildCaptcha(HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        // 随机生成验证码
        Captcha captcha = CaptchaUtil.randomCaptcha(CaptchaUtil.CaptchaType.ARITHMETIC, CaptchaUtil.CaptchaType.CHARACTER, CaptchaUtil.CaptchaType.CHARACTER_GIF);
        // 验证码图片
        String base64 = captcha.toBase64();
        // 答案
        String answer = captcha.text();
        // 将答案存入缓存
        cache.put(CacheKeyConstant.CAPTCHA_KEY + sessionId, answer);
        // 构建响应对象
        return new CaptchaVO(base64);
    }

    /**
     * 新用户注册
     *
     * @param req     用户注册信息
     * @param request 请求信息
     * @return 执行结果
     */
    @Override
    public boolean register(RegisterReq req, HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        // 校验验证码
        checkCaptcha(sessionId, req.getCode());
        // 校验邮箱是否重复
        boolean emailRepeat = userInfoService.checkRepeat(null, req.getEmail(), null);
        ExceptionUtil.assertion(emailRepeat, ErrorCodeEnum.USER_ERROR_A0154);
        // 加密密码
        String password = req.getPassword();
        String salt = BCrypt.gensalt();
        password = BCrypt.hashpw(password, salt);
        // 没输入用户名则生成随机用户名
        String userName = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER + RandomUtil.BASE_CHAR.toUpperCase(), 8);
        // 新增用户
        UserInfoSaveReq saveReq = new UserInfoSaveReq();
        saveReq.setUserName(userName);
        saveReq.setEmail(req.getEmail());
        saveReq.setPassword(password);
        saveReq.setState(UserStateEnum.DISABLE.getCode());
        saveReq.setSex(UserSexEnum.UNKNOWN.getCode());
        return userInfoService.save(saveReq);
    }

    /**
     * 用户登录
     *
     * @param loginInfo 用户信息
     * @param request   请求信息
     * @return 认证信息
     */
    @Override
    public TokenAuthVO login(LoginReq loginInfo, HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        // 校验登录次数
        checkSessionAttempts(sessionId, loginInfo.getCode());

        // 获取授权器
        TokenGranter granter = TokenGranterBuilder.getGranter(TokenGranterBuilder.DEFAULT_GRANTER);
        // 构建授权参数
        Map<String, Object> params = Maps.newHashMap();
        params.put(GranterParams.ACCOUNT, loginInfo.getAccount());
        params.put(GranterParams.PASSWORD, loginInfo.getPassword());
        params.put(GranterParams.CAPTCHA_ANSWER, loginInfo.getCode());
        TokenAuthVO grant = granter.grant(params);
        // 重置登录次数
        cache.invalidate(CacheKeyConstant.SESSION_ATTEMPTS + sessionId);
        cache.invalidate(CacheKeyConstant.SESSION_LAST_ACCESS_TIME + sessionId);
        return grant;
    }


    /**
     * 刷新token
     *
     * @return token
     */
    @Override
    public TokenAuthVO refresh() {
        UserInfo userInfo = commonService.currentUser();
        return null;
    }

    /**
     * 校验验证码
     *
     * @param sessionId     请求id
     * @param captchaAnswer 验证码
     */
    @Override
    public void checkCaptcha(String sessionId, String captchaAnswer) {
        // 通过请求id获取验证码
        Object answer = cache.getIfPresent(CacheKeyConstant.CAPTCHA_KEY + sessionId);
        // 校验验证码
        ExceptionUtil.isNull(answer, ErrorCodeEnum.USER_ERROR_A0242);
        // 删除验证码，防止反复利用
        cache.invalidate(CacheKeyConstant.CAPTCHA_KEY + sessionId);
        // 不区分大小写
        ExceptionUtil.assertion(!answer.toString().equalsIgnoreCase(captchaAnswer), ErrorCodeEnum.USER_ERROR_A0240);
    }

    /**
     * 发送邮箱验证码
     *
     * @param req     请求信息
     * @param request 请求信息
     * @return 执行结果
     */
    @Override
    public boolean sendEmailCode(EmailCodeReq req, HttpServletRequest request) {
        String sessionId = request.getSession().getId();
        // 校验验证码
        checkCaptcha(sessionId, req.getCode());
        // 查询平台内是否有注册这个邮箱
        boolean havEmail = userInfoService.checkRepeat(null, req.getEmail(), null);
        ExceptionUtil.assertion(!havEmail, ErrorCodeEnum.USER_ERROR_A0157);

        String key = CacheKeyConstant.EMAIL_CODE_KEY + req.getEmail();
        // 判断是否已经发送了邮箱验证码
        if (cache.getIfPresent(key) != null) {
            return true;
        }
        // 生成邮箱验证码
        String code = RandomUtil.randomString(RandomUtil.BASE_CHAR_NUMBER, 12);
        Mail mail = Mail.builder()
                .to(req.getEmail())
                .subject("邮箱验证码")
                .content("")
                .templateParams(Map.of("code", code))
                .templateName("email_code.html")
                .build();
        // 发送邮件
        emailService.sendMail(mail);
        // 放入缓存
        cache.put(key, code);
        return true;
    }

    /**
     * 重置密码
     *
     * @param req 请求参数
     * @return 执行结果
     */
    @Override
    public boolean resetPwd(ResetPwdReq req) {
        String email = req.getEmail();
        String emailCode = req.getEmailCode().trim();
        // 校验验证码
        Object answer = cache.getIfPresent(CacheKeyConstant.EMAIL_CODE_KEY + email);
        ExceptionUtil.isNull(answer, ErrorCodeEnum.USER_ERROR_A0242);
        // 删除验证码，防止反复利用
        cache.invalidate(CacheKeyConstant.EMAIL_CODE_KEY + email);
        // 不区分大小写
        ExceptionUtil.assertion(!answer.toString().equalsIgnoreCase(emailCode), ErrorCodeEnum.USER_ERROR_A0240);
        // 密码重置
        return userInfoService.resetPass(email, req.getNewPwd());
    }

    /**
     * 校验验证码
     *
     * @param sessionId     会话id
     * @param captchaAnswer 验证码答案
     */
    private void checkSessionAttempts(String sessionId, String captchaAnswer) {
        // 获取会话尝试次数
        Integer attempts = (Integer) cache.get(CacheKeyConstant.SESSION_ATTEMPTS + sessionId, k -> 0);
        // 次数 + 1
        cache.put(CacheKeyConstant.SESSION_ATTEMPTS + sessionId, attempts + 1);
        // 如果尝试次数 >= 5，锁 5 分钟
        if (attempts >= 5) {
            // 获取当前时间
            LocalDateTime now = LocalDateTime.now();
            // 获取上一次访问时间
            LocalDateTime lockTime = (LocalDateTime) cache.get(CacheKeyConstant.SESSION_LAST_ACCESS_TIME + sessionId, k -> {
                // 设置 5 分钟后的时间戳
                return LocalDateTime.now().plusMinutes(5);
            });
            // 未到达锁定时间，抛出异常
            ExceptionUtil.assertion(now.isBefore(lockTime), ErrorCodeEnum.USER_ERROR_A0211);
            // 到达锁定时间，继续往后延 5 分钟
            cache.put(CacheKeyConstant.SESSION_LAST_ACCESS_TIME + sessionId, now.plusMinutes(5));
        }
        // 如果尝试次数 >= 3，加入验证码环节
        if (attempts >= 3) {
            ExceptionUtil.isBlank(captchaAnswer, ErrorCodeEnum.USER_ERROR_A0243);
            // 校验验证码
            checkCaptcha(sessionId, captchaAnswer);
        }
    }
}
